package viper.sender;

import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Random;
import java.util.Vector;

import javax.media.Buffer;
import javax.media.CannotRealizeException;
import javax.media.CaptureDeviceInfo;
import javax.media.CaptureDeviceManager;
import javax.media.Control;
import javax.media.DataSink;
import javax.media.Format;
import javax.media.Manager;
import javax.media.MediaLocator;
import javax.media.NoDataSourceException;
import javax.media.NoProcessorException;
import javax.media.Processor;
import javax.media.ProcessorModel;
import javax.media.control.BufferControl;
import javax.media.format.AudioFormat;
import javax.media.protocol.BufferTransferHandler;
import javax.media.protocol.ContentDescriptor;
import javax.media.protocol.DataSource;
import javax.media.protocol.PushBufferDataSource;
import javax.media.protocol.PushBufferStream;

import viper.call.common.LiveStream;
import viper.main.StateManager;
import viper.main.ViperHeader;

/**
 * @author Kevin Sin
 * 
 * Revision number: 5
 * Date created: 3/21/10
 * Date of Last Back-up on SVN: 3/21/10
 * 
 * <p>CallSender is used for sending packets when in a call.
 * Its inner class Extractor is responsible for capturing from
 * the mic.  The Extractor extracts data stream from DataSource object 
 * and also doubles as the transmitter that sends each individual packetized 
 * data.
 */
public class CallSender implements Runnable {
	
	//-------------------Singleton structure----------------------
	public final static CallSender INSTANCE = new CallSender();
	private CallSender(){

	}
	//-------------------------------------------------------------
	//-------------------capturing variables-----------------------
	public MediaLocator mediaLocator = null;
	public DataSink dataSink = null;
	public Processor mediaProcessor = null;
	public Extractor extractor = null;

	private DataSource source = null;
	private DataSource digitizedAudioSource;

	private CaptureDeviceInfo cdi;
	
	byte[] calleeAddrBytes;

	//Define audio codec type
	private static final Format[] FORMATS = new Format[]{
		new AudioFormat(AudioFormat.MPEG,
				44100,
				16,
				2,
				AudioFormat.BIG_ENDIAN,
				AudioFormat.SIGNED,
				8,
				8000.0,
				Format.byteArray)
	};
	//Define output of Processor
	private static final ContentDescriptor CONTENT_DESCRIPTOR =
		new ContentDescriptor(ContentDescriptor.RAW); //we just want raw data buffers
	//-------------------------------------------------------------

	
	/* 
	 * Run is called whenever a call is started.
	 */
	public void run() {
		//Set type of audio format expected from capture device
		AudioFormat format = new AudioFormat(AudioFormat.LINEAR, //encoding type
				44100, //sampling frequency 
				16, //sample depth
				2); //channels

		// Assign first available capture device as the designated device
		Vector<?> devices = CaptureDeviceManager.getDeviceList(format);
		//System.out.println("::Retrieving list of available capture devices...");	
		cdi = (CaptureDeviceInfo) devices.elementAt(0);
		//System.out.println("::Capture device set with the following properties: ");
		
		//get the callee array from State manager for Extractor
		String ipString = (String)StateManager.INSTANCE.getCalleeArray()[0];
		calleeAddrBytes = UDPSender.ipStringToBytes(ipString);
		InetAddress destIP = UDPSender.ipBytesToInetAddress(
				UDPSender.ipStringToBytes(ipString)
		);
		int destPort = StateManager.VIPER_PORT;

		//Create DataSource from capture device and assign capture device DataSource to it
		try {
			source = Manager.createDataSource(cdi.getLocator());
			setDataSource(source);
			System.out.println("::Capture Device connected!");
			System.out.println("::DataSource Initialized!");
		} catch (NoDataSourceException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (NoProcessorException e) {
			e.printStackTrace();
		} catch (CannotRealizeException e) {
			e.printStackTrace();
		}

		//Begin encoding audio stream
		try {
			mediaProcessor.start(); //Start the Processor: encode audio data by defined codec
			digitizedAudioSource.start();
			System.out.println("::Audio Encoding Started...");
			extractor = new Extractor(digitizedAudioSource, destIP, destPort);
			//extractor.setDropRate(0.3);
			extractor.start();
			System.out.println("::Transmitting to " + destIP + 
					" on port " + destPort +"...");
		} catch (Throwable t) {
			t.printStackTrace();
		}

		System.out.println("CallSender started");
	}

	/**
	 * End is called whenever a call is ended.  Make sure all the objects
	 * for capturing, and the relay and callee info is cleared for future
	 * calls.
	 */
	public void end()
	{
		try {
			source.stop();
			source.disconnect();
		} catch (IOException e) {
			e.printStackTrace();
		}
		extractor.stop();
		mediaProcessor.stop();	
		mediaLocator = null;
		dataSink = null;
		mediaProcessor = null;
		extractor = null;
		source = null;		
		
		System.out.println("stop CallSender");
	}

	/**
	 * @param ds
	 * @throws IOException
	 * @throws NoProcessorException
	 * @throws CannotRealizeException
	 * 
	 * For assigning this as the data source.
	 */
	public void setDataSource(DataSource ds) throws IOException,
	NoProcessorException, CannotRealizeException {

		//Construct Processor that will handle encoding with specified codec.
		mediaProcessor = Manager.createRealizedProcessor(
				new ProcessorModel(ds, FORMATS, CONTENT_DESCRIPTOR));


		Control [] test = mediaProcessor.getControls();

		//Isolate the BufferControl controller from Processor and manually set buffer times.
		BufferControl bcon = (BufferControl)test[1];
		bcon.setBufferLength(16); //in ms

		System.out.println("::DEBUG, Buffer Length: "+bcon.getBufferLength());

		// Get the digitized and encoded data stream as a DataSource
		digitizedAudioSource = mediaProcessor.getDataOutput();
	}

	class Extractor implements BufferTransferHandler {

		//-------------------capturing variables--------------------
		DataSource ds;
		PushBufferStream streams[];
		Buffer buff;
		LiveStream ls;
		Buffer bufftemp;
		//----------------------------------------------------------

		//------------------sending variables----------------------
		DatagramSocket sock = StateManager.INSTANCE.getDatagramSocket();
		byte[][] divstore; //diversity
		int counter;
		byte[][] toSend;
		private int seqNum = 0;
		//----------------------------------------------------------
		
		
		//------------------callee variables-----------------------
		InetAddress destIP;
		int destPort;
		//----------------------------------------------------------
		
		//-------------------drop rate variables--------------------
		double drate;
		double[] drateArr;
		Random rgen;
		//----------------------------------------------------------
		
		
		//-------------------relay variables------------------------
		InetAddress[] relayIP_array;
		private InetAddress [] relaysIP;
		//----------------------------------------------------------

		
		/**
		 * @param src
		 * @param ip
		 * @param port
		 * 
		 * Constructor for Extractor
		 */
		public Extractor(DataSource src, InetAddress ip, int port){
			
			//setting capturing variables
			streams = ((PushBufferDataSource)src).getStreams(); //DataSource containing the digitized audio stream is a push-type stream
			streams[0].setTransferHandler(this); //only a single audio stream in the DataSource
			ds = src;
			buff = new Buffer();
			bufftemp = new Buffer();

			//setting callee variables
			destIP = ip;
			destPort = port;

			//setting drop rate variables
			drate = 0.0;
			drateArr = null;
			rgen = new Random();
			
			//setting relay variables
			if(StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_111){
				relaysIP = new InetAddress[1];				
			}
			else if(StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_232){
				drateArr = new double[3];
				drateArr[0] = 0.00;
				drateArr[1] = 0.00;
				drateArr[2] = 0.00;
				
				divstore = new byte[2][];
				relaysIP = new InetAddress[3];
			}
			else if(StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_364){
				drateArr = new double[6];
				divstore = new byte[3][];
				relaysIP = new InetAddress[6];
			}			
			ArrayList <Object[]> relaystemp = StateManager.INSTANCE.getRelays();
			for(int i=0;i<relaystemp.size();i++)
				relaysIP[i] = UDPSender.ipBytesToInetAddress(UDPSender.ipStringToBytes((String)(relaystemp.get(i)[0])));
			counter = 0;

			System.out.println("::Destination Host identified as: " + destIP + "/" + port);
		}

		public PushBufferStream getStream(){
			System.out.println("getStream callleeeeddd");
			return streams[0];
		}

		public void start() {
			try {
				ds.start();
			} catch (IOException e) {
				System.err.println(e);
			}
		}

		public void stop() {
			try {
				ds.stop();
			} catch (IOException e) {
				System.err.println(e);
			}
		}

		//------implementing BufferTransferHandler------------
		/* 
		 * This gets called when there's data pushed from 
		 * the PushBufferDataSource.
		 */
		public void transferData(PushBufferStream stream) {
			try {
				stream.read(buff);
			} catch (IOException e) {
				System.err.println(e);
				return;
			}
			transmit(buff);
		}
		//----------------------------------------------------

		/**
		 * @param rate
		 * Set the packet loss rate as a percentage.
		 */
		public void setDropRate(double rate){
			drate = rate;
			System.out.println("::Set Packet Loss Rate to: " + drate);
		}
		
		
		/**
		 * @param rate
		 * @return Returns true if packet isn't to be dropped, 
		 * false if it should be droppped.
		 */
		private boolean checkDrop(double rate){
			double roll = rgen.nextDouble();
			//System.out.println("::DEBUG, Packet Loss Rolled: " + roll);
			if(roll>rate)
				return true;
			else
				return false;
		}
		
		/**
		 * @param low
		 * @param high
		 */
		//TODO: comment this
		private void randomDelay(long low, long high){
			long delay = rgen.nextInt((int)high-(int)low)+(int)low;
			
			try{
				Thread.currentThread().sleep(delay);
				System.out.println("::DEBUG, Random Delay: "+delay+"ms");
			}catch(InterruptedException ie){
				System.out.println("Interrupted Exception during random delay.");
			}
		}

		
		/**
		 * Executes every time there's data available to be sent in 
		 * the PushBufferStream.
		 * Used to extract each Buffer in the stream and 
		 * convert it to byte[] for transmission.
		 */
		public void transmit(Buffer buffer) {
			/** Extracted Buffer/byte[] Debug Messages **/
			/*
			System.out.println("::DEBUG, Extracted Buffer Data:");
			System.out.println("  *Time Stamp: " + buffer.getTimeStamp());
			System.out.println("  *Sequence #: " + buffer.getSequenceNumber());
			System.out.println("  *Pertinent Data Length: " + buffer.getLength());
			System.out.println("  *Data Format: " + buffer.getFormat());
			System.out.println("  *Header: " + buffer.getHeader()); //32864
			System.out.println("  *Flags: " + buffer.getFlags());
			System.out.println("  *Actual Data Length: " + ((byte[])buffer.getData()).length);
			System.out.println("  *Offset to Pertinent Data: " + buffer.getOffset());
			*/
			
			//variables that get set every time an audio packet is sent
			byte[] toSend = (byte[])buffer.getData();
			long timeStamp = buffer.getTimeStamp() / 1000000;
			//System.out.println("DEBUG: time: " + timeStamp + ", " + System.currentTimeMillis());
			byte[] timeBytes = ViperHeader.longToByteArray(timeStamp);
			
			/** TRANSMIT CODE **/
			//(1,0,1) case
			if(StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_101){
				byte[] packetBytes = ViperHeader.makeCallReceivePacket(timeBytes, (byte)0, toSend);
				if(checkDrop(drate)){
					//randomDelay(50,100);
					UDPSender.sendUDP(destIP.getAddress(), packetBytes);
					//System.out.println("::DEBUG, Sent Packet...");
				}
			}

			//(1,1,1) case
			else if(StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_111){				
				byte[] packetBytes = ViperHeader.makeCallRelayPacket(calleeAddrBytes, timeBytes, (byte)0, toSend);
				if(checkDrop(drate)){
					UDPSender.sendUDP(relaysIP[0].getAddress(), packetBytes);
					//System.out.println("::DEBUG, Sent Packet...");
				}
				//else
					//System.out.println("::DEBUG, Dropped Packet...");
			}
			
			//(2,3,2) case
			else if(StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_232){
				divstore[counter] = toSend;
				if(counter<1)
					counter++;
				else{ //we have enough for diversity coding, perform it
					byte[][] divsend = viper.diversity.Diversity.diversify(divstore[0], divstore[1]);
					
					//only send everything once we have enough packets to perform diversity coding
					counter = 0;
					byte [] packetBytes;
					// create packet for each relay with appropriate seq#, pkt type, and final destination IP
					for(int i=0;i<3;i++){
						packetBytes = ViperHeader.makeCallRelayPacket(calleeAddrBytes, timeBytes, (byte)i, divsend[i]);
						if(checkDrop(drateArr[i])){
							//UDPSender.sendUDP(destIP.getAddress()/*relaysIP[0].getAddress()*/, packetBytes); //Test Case, one relay
							UDPSender.sendUDP(relaysIP[i].getAddress(), packetBytes);
							//System.out.println("::DEBUG, Sent Packet...");
						}
						//else
							//System.out.println("::DEBUG, Dropped Packet...");	
					}
					seqNum++;
				}
			}
			
			//(3,6,4) case
			else if(StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_364){
				divstore[counter] = toSend;
				if(counter<2)
					counter++;
				else{ //we have enough for diversity coding, perform it	
					byte[][] divsend = viper.diversity.Diversity.diversify(divstore[0], divstore[1], divstore[2]);
					
					//only send everything once we have enough packets to perform diversity coding
					counter = 0;
					byte [] packetBytes;
					// create packet for each relay with appropriate seq#, pkt type, and final destination IP
					for(int i=0;i<6;i++){
						packetBytes = ViperHeader.makeCallRelayPacket(calleeAddrBytes, timeBytes, (byte)i, divsend[i]);
						if(checkDrop(drateArr[i])){
							UDPSender.sendUDP(relaysIP[i].getAddress(), packetBytes);
							//System.out.println("::DEBUG, Sent Packet...");
						}
						//else
							//System.out.println("::DEBUG, Dropped Packet...");
					}
					seqNum++;
				}
			}
		}
	}
}